#
# OSZTrap
# Copyright (C) 2010 Open Sistemas <imasd@opensistemas.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
package OpenSistemas::OSZTrap;

use strict;
use warnings;
use Carp;
use Sys::Hostname;
use Net::SNMP;

our $VERSION = "1.0.1";

sub new
{
  my $pkg = shift;

  my $self = {
    heartbeatOID => '.1.3.6.1.4.1.35633.1.1.1.0.1',
    alarmOID => '.1.3.6.1.4.1.35633.1.1.1.1',
    sessions => {},
    timeout => 60,
    pollInterval => 30,
    uptimeBase => 0,
    lastEventId => 0,
    sysname => hostname(),
    community => 'public',
    dbh => undef,
    compatibilty => 'unknown',
    pollQuery => undef,
    end => 0
  };

  open(UPTIME, "</proc/uptime")
    or croak("Failed to open /proc/uptime");
  my $line = <UPTIME>;
  close(UPTIME);

  ($self->{uptimeBase}) = split(' ', $line);
  $self->{uptimeBase} = int(time() - $self->{uptimeBase});

  $self->{lastEventId} = $self->{uptimeBase};

  bless $self, $pkg;
  return $self;
}

sub session
{
  my $self = shift;
  my $hostname = shift;
  my $port = shift;

  my ($session, $error) = Net::SNMP->session(
    -domain => 'udp',
    -hostname => $hostname,
    -port => $port,
    -community => $self->{community},
    -version => 2
  );

  return $error if !$session;

  $self->{sessions}->{$hostname .':' .$port} = $session;
  return undef;
}

sub closeSessions
{
  my $self = shift;
  my %sessions = %{$self->{sessions}};

  foreach (keys(%sessions))
  {
    $sessions{$_}->close();
  }

  %sessions = ();
}

sub connection
{
  my $self = shift;

  croak 'Missing parameter' if !@_;

  $self->{dbh} = shift;

  # guess zabbix version
  my $dbh = $self->{dbh};

  my %vers = (
    '1.4.x' => "select a.alertid, a.status, t.priority, i.key_, h.host from alerts a, triggers t, media_type mt, functions f, items i, hosts h where mt.description='SNMP-TRAP' and mt.mediatypeid=a.mediatypeid and t.triggerid=a.triggerid and f.triggerid=t.triggerid and i.itemid=f.itemid and h.hostid=i.hostid and a.clock>=unix_timestamp()-? order by a.alertid;",
    '1.8.x' => "select a.alertid, a.status, t.priority, i.key_, h.host from alerts a, events e, triggers t, media_type mt, functions f, items i, hosts h where mt.description='SNMP-TRAP' and mt.mediatypeid=a.mediatypeid and e.eventid=a.eventid and e.objectid=t.triggerid and f.triggerid=t.triggerid and i.itemid=f.itemid and h.hostid=i.hostid and a.clock>=unix_timestamp()-? order by a.alertid;"
  );

  foreach(keys(%vers))
  {
    my $sth = $dbh->prepare($vers{$_});
    if($sth->execute($self->{pollInterval}))
    {
      $sth->finish();
      $self->{compatibility} = $_;
      $self->{pollQuery} = $vers{$_};
      return;
    }
  }

  # fall back to 1.8.x
  $self->{compatibility} = '1.8.x';
  $self->{pollQuery} = $vers{'1.8.x'};
}

sub compatibility
{
  my $self = shift;

  croak 'Connect to the database firts' if !$self->{compatibility};

  return 'running in ' .$self->{compatibility} .' mode';
}

sub disconnect
{
  my $self = shift;

  croak 'No open connection' if !$self->{dbh};

  $self->{dbh}->disconnect();
}

sub sysname
{
  my $self = shift;

  if(@_)
  {
    $self->{sysname} = shift;
  }
  else
  {
    return $self->{sysname};
  }
}

sub community
{
  my $self = shift;

  if(@_)
  {
    $self->{community} = shift;
  }
  else
  {
    return $self->{community};
  }
}

sub timeout
{
  my $self = shift;

  if(@_)
  {
    $self->{timeout} = shift;
  }
  else
  {
    return $self->{timeout};
  }
}

sub pollInterval
{
  my $self = shift;

  if(@_)
  {
    $self->{pollInterval} = shift;
  }
  else
  {
    return $self->{pollInterval};
  }
}

sub poll
{
  my $self = shift;

  croak 'Connect to the database first' if !$self->{dbh};

  my $dbh = $self->{dbh};
  my %sessions = %{$self->{sessions}};

  my $sth = $dbh->prepare($self->{pollQuery});
  return $dbh->errstr if !$sth;
  return $dbh->errstr if !$sth->execute($self->{pollInterval});

  while(my $ref = $sth->fetchrow_hashref())
  {
    foreach (keys(%sessions))
    {
      my $uptime = time() - $self->{uptimeBase};
      my $result = $sessions{$_}->snmpv2_trap(
        -varbindlist      => [
          ('.1.3.6.1.2.1.1.3.0', TIMETICKS, $uptime),
          ('.1.3.6.1.6.3.1.1.4.1.0', OBJECT_IDENTIFIER, $self->{alarmOID}),
          ('.1.3.6.1.4.1.35633.1.1.1.0.2.1.1', GAUGE, $ref->{'alertid'}),
          ('.1.3.6.1.4.1.35633.1.1.1.0.2.1.2', OCTET_STRING, $ref->{'key_'}),
          ('.1.3.6.1.4.1.35633.1.1.1.0.2.1.3', INTEGER, $ref->{'priority'}),
          ('.1.3.6.1.4.1.35633.1.1.1.0.2.1.4', INTEGER, $ref->{'status'}),
          ('.1.3.6.1.4.1.35633.1.1.1.0.2.1.5', OCTET_STRING, $ref->{'host'}),
          ('.1.3.6.1.2.1.1.5', OCTET_STRING, $self->{sysname}),
        ]
      );
      return $sessions{$_}->error() if !$result;
    }
  }
  $sth->finish();

  return undef;
}

sub trap
{
  my $self = shift;
  my %sessions = %{$self->{sessions}};

  foreach (keys(%sessions))
  {
    my $uptime = time() - $self->{uptimeBase};
    my $result = $sessions{$_}->snmpv2_trap(
      -varbindlist      => [
        ('.1.3.6.1.2.1.1.3.0', TIMETICKS, $uptime),
        ('.1.3.6.1.6.3.1.1.4.1.0', OBJECT_IDENTIFIER, $self->{heartbeatOID}),
        ('.1.3.6.1.4.1.35633.1.1.1.1.1', GAUGE, $self->{lastEventId}++),
        ('.1.3.6.1.4.1.35633.1.1.1.1.2', GAUGE, $self->{timeout}),
        ('.1.3.6.1.2.1.1.5', OCTET_STRING, $self->{sysname}),
      ]
    );
    return $sessions{$_}->error() if !$result;
  }

  return undef;
}

sub end
{
  my $self = shift;

  $self->{end} = 1;
}

sub running
{
  my $self = shift;

  return $self->{end} == 0;
}

=pod

=head1 NAME

OpenSistemas::OSZTrap

=head1 SYNOPSIS

    use OpenSistemas::OSZTrap;

    $app = OpenSistemas::OSZTrap->new();
    $app->session('192.168.1.100', '162');
    $app->connection($dbh);

    $app->trap();
    $app->poll();

    $app->closeSessions();
    $app->disconnect();

=head1 DESCRIPTION

This is the application that provides a SNMP trap interface for Zabbix. You may run osztrapd instead.

=head1 METHODS

=head2 new()

    my $app = OpenSistemas::OSZTrap->new();

=head2 session(hostname, port)

    my $error = $app->session('192.168.1.100', '162');

Returns an error string, or C<undef> if no error.

=head2 closeSessions()

    $app->closeSessions();

Closes all the open SNMP sessions.

=head2 connection(handler)

    $app->connection($dbh);

Setup the DBI connection handler, already connected to the Zabbix database. It also checks
the sechema of the database.

=head2 disconnect()

    $app->disconnect();

Disconnect from the Zabbix database.

=head2 compatibility()

Returns a sting with the Zabbix compatibility mode (1.4.x or 1.8.x).

=head2 sysname()

=head2 sysname(name)

    my $current = $app->sysname();
    $app->sysname('newname');

Get/set the system name, used in the SNMP trap.

=head2 community()

=head2 community(name)

    my $current = $app->community();
    $app->community('private');

Get/set the community, used in the SNMP sessions. By default C<public> community is used.

=head2 timeout()

=head2 timeout(seconds)

    my $seconds = $app->timeout();
    $app->timeout(120);

Get/set the timeout seconds to send the heartbeat.

=head2 pollInterval()

=head2 pollInterval(seconds)

    my $seconds = $app->pollInterval();
    $app->pollInterval(120);

Get/set the poll interval in seconds to check for new events in the Zabbix database.

=head2 end()

=head2 running()

    $app->end();
    print "is running" if $app->running();

C<end()> sets the application to end, and C<running()> returns true if the application should be running.

=head2 trap()

=head2 poll()

    $app->trap();
    $app->poll();

C<trap()> is used to send the heartbeat trap, and C<poll()> it's used to poll the Zabbix
database for events and, if there's a new event, send the required trap.

=head1 AUTHOR

This module has been written by Juan J. Martinez <jjmartinez@opensistemas.com>.

=head1 LICENSE

This application it's free software and it's published under the GPL3 license.

You should have received a copy of the GNU General Public License along with this program.
If not, see <http://www.gnu.org/licenses/>.

=head1 SEE ALSO

osztrapd(1), OSZTRAP-MIB.

Project website: http://snmp4zabbix.opensistemas.com/

=cut

1;
